/*
 * ModelCC, distributed under ModelCC Shared Software License, www.modelcc.org
 */

package org.modelcc.io.java;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.modelcc.language.metamodel.MemberCollectionType;

/**
 * Reflection utilities
 * 
 * @author Luis Quesada (lquesada@modelcc.org) & Fernando Berzal (fberzal@modelcc.org)
 */
public class Reflection
{

    
	public static Field findField (Class c,String fieldName) 
	{
        try {
			return c.getDeclaredField(fieldName);
		} catch (Exception e) {
			if (c.getSuperclass()!=null)
				return findField(c.getSuperclass(),fieldName);
            return null;
        }
	}
	
	public static Field[] getAllFields(Class type) 
	{
		List<Field> list = new ArrayList<Field>();
		getAllFields(list,type);
		Field[] fl = new Field[list.size()];
		for (int i = 0;i < list.size();i++)
			fl[i] = list.get(i);
		return fl;
	}

	
	private static List<Field> getAllFields(List<Field> fields, Class type) 
	{
	    for (Field field: type.getDeclaredFields()) {
	        fields.add(field);
	    }

	    if (type.getSuperclass() != null) {
	        fields = getAllFields(fields, type.getSuperclass());
	    }

	    return fields;
	}
	
	// Class cache
	
	private static Map<String, Set<Class>> cache = new HashMap<String, Set<Class>>();

    /**
     * Detect all the classes within a specified package or its subpackages.
     * 
     * @param packageName the package name
     * @return a set of classes
     * @throws ClassNotFoundException
     */
    public static Set<Class> findClasses (String packageName, Class type) 
    	throws ClassNotFoundException 
    {
    	// Cache lookup 
    	
    	Set<Class> classes = cache.get(packageName);
    	    	
    	if (classes == null) {  
    		
    		// Cache miss: Look for classes
    		
    		URL url = getResourceURL(packageName);

    		if (url != null) {

    			classes = new HashSet<Class>();

    			File directory = new File(url.getFile());

    			if (directory.exists()) { // class within the file system

    				String[] files = directory.list();

    				for (int i=0;i<files.length;i++) {
    					if (files[i].endsWith(".class") && !files[i].contains("<error>")) {
    						String classname = files[i].substring(0,files[i].length()-6);
    						Class newClass = Reflection.getClass(packageName+"."+classname);
    						if (newClass != null)
    							classes.add(newClass);
    					} else {
    						classes.addAll(findClasses(packageName+"."+files[i],type));
    					}
    				}

    			} else { // class within a jar file

        			classes = findJarClasses(type);                        
    			}
    		}
    		
    		// Cache update
    		
    		cache.put(packageName, classes);
    	}
    	
        return classes;
    }

    /**
     * Find classes in the same JAR file than the specified class.
     * 
     * @param type the class
     * @return classes in the JAR file
     * @throws ClassNotFoundException
     */
	private static Set<Class> findJarClasses(Class type)
		throws ClassNotFoundException 
	{
		Set<Class> classes = new HashSet<Class>();

		JarInputStream jarFile;

		try {

			jarFile = new JarInputStream(new FileInputStream(type.getProtectionDomain().getCodeSource().getLocation().toString().substring(5)));

			try {
				JarEntry e = jarFile.getNextJarEntry();
				
				while (e != null) {
					String entryname = e.getName();
					if (entryname.endsWith(".class") && !entryname.contains("<error>")) {
						String classname = entryname.substring(0,entryname.length()-6);
						if (classname.startsWith("/")) {
							classname = classname.substring(1);
						}
						classname = classname.replace('/','.');
						Class newClass = Reflection.getClass(classname);
						if (newClass != null)
							classes.add(newClass);
					}
					e = jarFile.getNextJarEntry();
				}
				
			} catch (IOException error) {
			}

			jarFile.close();

		} catch (IOException error) {
		}
		
		return classes;
	}

	/**
	 * Detect all the classes that extend a class or implement an interface.
     * 
	 * @param type the class
	 * @return a set of subclasses
	 * @throws ClassNotFoundException
	 */	
    public static Set<Class> findSubclasses (Class type)
    	throws ClassNotFoundException 
    {
		String packageName = "";
		
		if (type.getPackage() != null)
			packageName = type.getPackage().getName();
			
		return findSubclasses(packageName,type);
    }
    
	/**
	 * Detect all the classes that extend a class or implement an interface within a specified set of packages.
     * 
	 * @param packages the set of package names
	 * @param type the class
	 * @return a set of subclasses
	 * @throws ClassNotFoundException
	 */	
    public static Set<Class> findSubclasses (Set<String> packages, Class type)
    	throws ClassNotFoundException 
    {
        Set<Class> subclasses = new HashSet<Class>();
        
        for (String packageName : packages) {
        	subclasses.addAll(findSubclasses(packageName,type));
        }
        
        return subclasses;
    }

    /**
     * Detect all the classes that extend a class or implement an interface 
     * within a specified package or its subpackages.
     * 
     * @param packageName the package name
     * @param type the class
     * @return a set of subclasses
     * @throws ClassNotFoundException
     */
    public static Set<Class> findSubclasses(String packageName, Class type) throws ClassNotFoundException 
    {
    	Set<Class> classes = findClasses(packageName,type);
		Set<Class> subclasses = new HashSet<Class>();
    	
    	for (Class candidate: classes) {
			if (type.isAssignableFrom(candidate) && !type.equals(candidate)) {
				subclasses.add(candidate);
			}
    	}
    	    	
        return subclasses;
    }

    
	private static URL getResourceURL (String packageName) 
	{
		String name = packageName;
		
		if (!name.startsWith("/"))
			name = "/" + name;
		
		name = name.replace('.','/');
		
		return Reflection.class.getResource(name);
	}

    
	public static boolean hasConstructor(Class elementClass) 
	{
		try {
			if (elementClass.getConstructor() != null)
				return true;
		} catch (Exception e) {
	
		}
		return false;
	}

	/**
	 * Checks whether a class (or one of its members) has a given annotation
	 * @param type the class
	 * @param annotation the annotation
	 * @return true if it has a pattern or value annotation, false otherwise
	 */
	public static boolean hasAnnotation (Class type, Class annotation) 
	{
		if (type.isAnnotationPresent(annotation))
			return true;
		else {
			Field fl[] = type.getDeclaredFields();
			int i;
			for (i=0; i<fl.length;i++) {
				if (fl[i].isAnnotationPresent(annotation)) {
					return true;
				}
			}
			return false;
		}
	}

	/**
	 * Returns an element type
	 * @param collection the collection type (null if no collection)
	 * @param field the field
	 * @return the component type
	 */
	public static Class getType(MemberCollectionType collection, Field field) 
	{
		if (collection == null)
			return field.getType();
	
		switch (collection) {
		case ARRAY:
			return field.getType().getComponentType();
		case LIST:
		case SET:
			String name = field.getType().getName();
			String fname = field.getGenericType().toString();
			int ini = name.length()+1;
			int fin = fname.length()-1;
			if (ini < fin && fname.endsWith(">")) {
				try {
					return Reflection.getClass(fname.substring(ini, fin));
				} catch (ClassNotFoundException ex) {
					return null;
				}
			}
		}
	
		return null;
	}

	/**
	 * Obtain the Class object corresponding to a primitive or non-primitive type name.
	 * @param className the class name
	 * @return the Class object
	 * @throws ClassNotFoundException
	 */
	public static Class getClass(String className) throws ClassNotFoundException {
	    if (!className.contains(".")) {
	        if("int" .equals(className)) return int.class;
	        if("long".equals(className)) return long.class;
	        if("byte".equals(className)) return byte.class;
	        if("short".equals(className)) return short.class;
	        if("float".equals(className)) return float.class;
	        if("double".equals(className)) return double.class;
	        if("boolean".equals(className)) return boolean.class;
	        if("char".equals(className)) return char.class;
	    }
	    return Class.forName(className);
	}

}
